PHP Project Automation with Bob
After not being satisfied with Ant, Phing and Pake — I’ve scratched my own itch and made Bob — a simple project automation tool.
Bob was written as a tool for me to automate those tiny, repetitive tasks that pop up every once in a while — like commands which have to be run on the shell every time with the same arguments, or some maintenance for the database.
These task do almost never need a fancy CLI frontend. They just need a place where they can be put and simple means to link them together.
Here is an uncomplete list of possible use cases right from the top of my head:
- Generating an archive of all files and upload it somewhere for deployment.
- Running all tests (maybe with sub-tasks to run only unit or integration tests).
- Code Generation (often created application artifacts like controllers, domain classes).
- Launching a development server.
- Doing Database migrations.
- Automating certain maintenance stuff.
- Doing tasks necessary when a new release is due, for example bump the application’s version number, create some tags in the VCS and make a tarball.
I wrote Bob because I had enough of the verboseness of Phing’s and Ant’s XML based DSLs and wasn’t quite satisfied with Pake’s PHP based DSL either.
I had these Design Goals in mind for Bob:
- The Build configs must be plain PHP files, so existing application code, libraries and functions can be easily used. This should also lower the barrier of contribution for the project’s team members in a PHP project. When the barrier of contribution is too high build files start to rot. I’m seeing this happen with our Ant Build Configs at work.
- Shell scripts are easy and simple, but not an option because of their portability (mainly Windows).
- I wanted to make use of Closures for defining task actions, to get defining tasks as concise as possible with PHP. I really disliked the way Pake does not use Closures (since it has to support PHP 5.2.x) and instead forces me to define a separate, named function for the task action which leads — in my opinion — to unnecessary verboseness.
- I really like Rake, therefore it influenced the design of Bob and its DSL quite a bit.
- Keep it simple. The tool should not have more than several hundred lines of code.
- I wanted to experiment with an own coding style, I would like to call it “Lean PHP”. I plan to do an extra article on that.
Install
The recommended way of installing Bob is through Composer.
Create a composer.json
in your project’s root and put this
into it:
{
"require": {
"chh/bob": "master-dev"
}
}
Then run on the shell:
$ wget http://getcomposer.org/composer.phar
php composer.phar install
Bob is then ready to use in vendor/bin/bob.phar
.
Quickstart
To start using Bob, create a bob_config.php
in your project’s root
directory. This file will contain all tasks.
This is how a simple bob_config.php
can look like:
<?php
namespace Bob;
task('hello', function() {
println('Hello World');
});
task('foo', function() {
println('Foo');
});
To run this task launch Bob with hello
as argument. Bob treats all
arguments passed after the flags as task names. These tasks are run in
the specified order:
$ php vendor/bin/bob.phar hello
Hello World
$ php vendor/bin/bob.phar hello foo
Hello World
Foo
When no task is given on the command line, then Bob attempts to run the
default
task.
Tasks can be described by using the desc()
function before defining
a task. Now let’s add a simple description to our hello
task:
desc('Echoes Hello World');
task('hello', function() {
...
});
Descriptions are shown alongside their tasks when Bob is run with the
--tasks
flag:
$ php vendor/bin/bob.phar --tasks
(in yourproject/)
bob hello # Echoes Hello World
To make dependencies between tasks, just pass an array of task names as
second argument to the call to task
.
Let’s change the definition of our first task to reference the foo
task as a dependency:
task('hello', array('foo'), function() {
println('Hello World')
});
And now run Bob again:
$ php vendor/bin/bob.phar hello
Foo
Hello World
We can use this now to set the hello
task as default task, by adding
it as dependency:
task('default', array('hello'));
When we run Bob without task names then the hello
task is now run:
$ php vendor/bin/bob.phar
Foo
Hello World
This was only a quick overview of what Bob is able to do. To see more examples, read on in the next section.
Practical Recipes
These are some common scenarios for using Bob. More examples can be found in Bob’s own bob_config.php.
Update composer packages when the composer.json has changed
This is good example for a file task. Here we run composer.phar update
when the composer.json
is newer than the composer.lock
created by
composer. This keeps our dependencies up to date and could be run before
running the unit tests.
fileTask('composer.lock', array('composer.json'), function() {
sh('composer.phar update');
});
Running unit tests
This runs all tests with PHPUnit and copies the provided
phpunit.dist.xml
to a phpunit.xml
before running the tests.
task('test', array('phpunit.xml'), function() {
sh('phpunit');
});
# Copies the phpunit.dist.xml to phpunit.xml, when phpunit.xml does
# not exist.
fileTask('phpunit.xml', array('phpunit.dist.xml'), function() {
copy('phpunit.dist.xml', 'phpunit.xml');
});
Running a local PHP development server
This requires PHP 5.4.0 at least for the builtin CLI Web Server.
task('server', function() {
# The Symfony Process Component is included with Bob.
$phpFinder = new \Symfony\Component\Process\PhpExecutableFinder;
$php = $phpFinder->find();
$process = new \Symfony\Component\Process\Process(join(' ', array(
$php, '-S', 'localhost:8080', '-t', 'public/', 'public/index.php'
)));
$process->setTimeout(null);
# Write Error output to STDERR and normal output to
# STDOUT, so we see the server's logging messages:
$process->run(function($type, $output) {
$type == 'err' ? fwrite(STDERR, $output) : echo $output;
});
});